/*
 * ============================================================================
 *
 *  SourceMod Project Base
 *
 *  File:          menulib.inc
 *  Type:          Library
 *  Description:   A menu system that multiple modules can build upon.
 *
 *  Copyright (C) 2009-2011  Greyscale
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * ============================================================================
 */

#if defined _menulib_included
 #endinput
#endif
#define _menulib_included

// Include libraries.
#include "zr/libraries/utilities"

/**
 * The max string/array lengths of menu data variables.
 */
#define ML_DATA_ID          128
#define ML_DATA_TITLE       128
#define ML_DATA_LABEL       64
#define ML_DATA_INFO        128

/**
 * Menu callback
 * Called right before the menu is displayed to a client.  Menu data changed here will show up in the menu.
 * 
 * @param menu      The menulib menu handle.
 * @param client    The client index.
 */
functag public MenuLibPreCallback(Handle:menu, client);

/**
 * Menu callback
 * Called for all MenuAction actions except voting.  See menus.inc.  Menu data changed here won't be in time to show in the menu.
 * 
 * @param menu      The menulib menu handle.
 * @param action    Action client is doing in menu.  May be any action except voting ones.  MenuAction_Select can be handled in other callbacks.
 * @param client    The client index.
 * @param slot      The menu slot selected. (starting from 0)
 */
functag public MenuLibCallback(Handle:menu, MenuAction:action, client, slot);

/**
 * Menu callback
 * Called when a certain button in a menu is pressed.  The menu action is always MenuAction_Select.  This happens right after MenuLibCallback
 * 
 * @param menu      The menulib menu handle.
 * @param client    The client index.
 * @param slot      The menu slot selected. (starting from 0)
 */
functag public MenuLibBtnCallback(Handle:menu, client, slot);

/**
 * Called for each client added to the menu being generated by MenuLib_GenerateClientMenu. 
 *
 * @param menu          Menu handle the clients are being added to.
 * @param client        Index of client that is about to be added to the menu.
 * @param buttontxt     The text that will be shown for this client's button.
 *                      Default is just the client's in-game name.
 * @param info          The button's information string.  Be careful modifying this, it will break MenuLib_GetClientIndex so you will have to interpret this data yourself.
 *
 * @return              Follows rules of a normal hook.  (Plugin_Stop will behave the same as Plugin_Handled)
 */
functag public Action:MenuLibClientFilter(Handle:menu, client, String:buttontxt[], String:info[]);

/**
 * Menu data.
 */
enum MenuLib_Menu
{
    String:MenuData_Id[ML_DATA_ID],             // Unique string for this menu.
    MenuLibPreCallback:MenuData_PreCallback,    // Function called right before the menu is sent.  Set to INVALID_FUNCTION for no callback.
    MenuLibCallback:MenuData_Callback,          // A function called for certain menu events.  See enum MenuAction in menus.inc.  This is called for all actions except voting and MenuAction_Select.
    String:MenuData_Title[ML_DATA_TITLE],       // Title of the menu.
    bool:MenuData_Translate,                    // True to read the title as a translations phrase, false to show text as menu title.
    bool:MenuData_ForceBack,                    // Force the back button to appear in the menu.  This will override menulib's automatic handling of the back button to allow your own code.  See MenuLib_MenuGoBack().
    bool:MenuData_Temp,                         // Temporary menus are deleted as soon as an option is selected.
    Handle:MenuData_Handle                      // (READ-ONLY) A valid handle returned from most recent call to SM native DisplayMenu for this menu, INVALID_HANDLE otherwise.
}

/**
 * What menu should a button open.
 */
enum MenuLib_BtnNextMenu
{
    BtnNextMenu_None,           // Don't open any menu when this button is used.
    BtnNextMenu_LinkMenu,       // Define a new menu to open.
    BtnNextMenu_LinkCurrent,    // Resend current menu.  This option is invalid for temp menus.
    BtnNextMenu_LinkBack        // Send previous menu.  If there is no previous menu, it will resend the current one.
}

/**
 * Information about a single menu button.
 */
enum MenuLib_Button
{
    String:MenuBtn_Label[ML_DATA_LABEL],    // What to display for this menu button.
    String:MenuBtn_Info[ML_DATA_INFO],      // Identical to the "info" parameter in AddMenuItem, see that.
    bool:MenuBtn_Translate,                 // Instead of printing MenuBtn_Label, it will print the translation phrase it points to.
    MenuBtn_Style,                          // See SM include menus.inc ITEMDRAW_* defines.
    MenuLibBtnCallback:MenuBtn_Callback,    // Function to call when this button is selected.  INVALID_FUNCTION to call no function.
    MenuLib_BtnNextMenu:MenuBtn_NextMenu,   // See enum MenuLib_BtnNextMenu.
    String:MenuBtn_LinkedMenu[ML_DATA_ID]   // If MenuBtn_NextMenu = BtnNextMenu_LinkMenu, this is the menu that is linked.
}

/**
 * Keeps track of the last menu the user had open.
 */
new Handle:g_hPrevMenus[MAXPLAYERS + 1];

/**
 * Keeps a cache of all current menus that are looked up by their id.
 */
new Handle:g_hMenuLibCache;

/**
 * Dummy array used as a way to count the cells required to store menu data.
 */
static stock g_DummyMenuData[MenuLib_Menu];
static stock g_DummyMenuBtnData[MenuLib_Button];

/**
 * Creates a menu ready to be filled with menu buttons.
 * 
 * @param id            A unique string identifier for the menu. The identifier is immutable.
 * @param precallback   Called right before the menu is displayed to a client.  INVALID_FUNCTION for no callback.
 * @param callback      Called for all MenuAction events except voting.  INVALID_FUNCTION for no callback.
 * @param title         The menu title.
 * @param translate     Whether to treat title as a translations phrase or not.
 * @param forceback     Force the back button to appear in the menu.
 * @param temp          Delete menu as soon as an option is selected. (Most
 *                      auto-generated menus use this.)
 * 
 * @return              Handle to menu.  INVALID_HANDLE if the given id is empty or
 *                      already taken.
 */
stock Handle:MenuLib_CreateMenu(const String:id[], MenuLibPreCallback:precallback, MenuLibCallback:callback, const String:title[], bool:translate, bool:forceback, bool:temp)
{
    // Check if id is empty.
    if (!id[0])
        return INVALID_HANDLE;
    
    // Initialize if not already.
    if (g_hMenuLibCache == INVALID_HANDLE)
        g_hMenuLibCache = CreateTrie();
    else
    {
        // Check if this unique id is available.
        if (MenuLib_FindMenuById(id) != INVALID_HANDLE)
            return INVALID_HANDLE;
    }
    
    // Use the max value of both data structures.
    new high = sizeof(g_DummyMenuData);
    if (sizeof(g_DummyMenuBtnData) > high) high = sizeof(g_DummyMenuBtnData);
    new Handle:hMenu = CreateArray(high);
    
    // Create menu data object.
    new menudata[MenuLib_Menu];
    strcopy(menudata[MenuData_Id], sizeof(menudata[MenuData_Id]), id);
    menudata[MenuData_PreCallback] = precallback;
    menudata[MenuData_Callback] = callback;
    strcopy(menudata[MenuData_Title], sizeof(menudata[MenuData_Title]), title);
    menudata[MenuData_Translate] = translate;
    menudata[MenuData_ForceBack] = forceback;
    menudata[MenuData_Temp] = temp;
    
    PushArrayArray(hMenu, menudata[0], sizeof(menudata));
    SetTrieValue(g_hMenuLibCache, id, hMenu);
    return hMenu;
}

/**
 * Returns a MenuLib menu handle given it's unique id.
 * 
 * @param id    The unique id of the sought menu.
 * 
 * @return      Handle belonging to the given unique id.  INVALID_HANDLE if not found.
 */
stock Handle:MenuLib_FindMenuById(const String:id[])
{
    new Handle:hMenu;
    if (!GetTrieValue(g_hMenuLibCache, id, hMenu))
        return INVALID_HANDLE;
    return hMenu;
}

/**
 * Deletes a menu page and/or all menu pages it links to.
 * This will set each page's LinkedMenu value to INVALID_HANDLE as it kills them.
 * Warning: The root menu along with menus not in this chain may still be linked to menus that were deleted
 * in which case you will most likely get an error on the 2nd line of MenuLib_GetMenuTitle!
 * Always use this to delete these menus, don't just close the handle!
 * 
 * @param hMenu         The handle to the menu page to delete.
 * @param recursive     True to delete all menus this links to, false to just delete this menu.
 * 
 * @error   Invalid menu handle.
 */
stock MenuLib_DeleteMenu(Handle:hMenu, bool:recursive = false)
{
    if (hMenu == INVALID_HANDLE)
        ThrowError("Invalid menu handle: %x", hMenu);
    
    if (recursive)
    {
        // Have to keep track of killed handles because I can't know if it's valid..
        new Handle:hKilledHandles = CreateArray();
        MenuLib_RecursiveDelete(hMenu, hKilledHandles);
        CloseHandle(hKilledHandles);
    }
    else
        MenuLib_EradicateMenu(hMenu);
}

static stock MenuLib_RecursiveDelete(Handle:hMenu, Handle:hKilledHandles)
{
    // We killed this one already.
    if (FindValueInArray(hKilledHandles, hMenu) > -1)
        return;
    
    new menubtndata[MenuLib_Button];
    new Handle:linkedmenu;
    new count = MenuLib_GetMenuBtnCount(hMenu);
    for (new bindex = 0; bindex < count; bindex++)
    {
        // Get all info about this menu button.
        MenuLib_BtnReadAll(hMenu, bindex, menubtndata);
        
        // If it links to another menu, recurse into it.
        linkedmenu = MenuLib_FindMenuById(menubtndata[MenuBtn_LinkedMenu]);
        if (linkedmenu != INVALID_HANDLE)
        {
            MenuLib_RecursiveDelete(linkedmenu, hKilledHandles);
            menubtndata[MenuBtn_LinkedMenu][0] = '\0';
            MenuLib_BtnWriteAll(hMenu, bindex, menubtndata);
        }
    }
    MenuLib_EradicateMenu(hMenu);
    PushArrayCell(hKilledHandles, hMenu);
}

static stock MenuLib_EradicateMenu(Handle:hMenu)
{
    // Look through all menus and unlink this menu from all to tie up loose ends.
    // ..Can't iterate tries >:|
    
    // Remove from cache/lookup table.
    decl String:id[ML_DATA_ID];
    MenuLib_MenuReadString(hMenu, MenuData_Id, id, sizeof(id));
    RemoveFromTrie(g_hMenuLibCache, id);
    
    // Destroy menu data.
    CloseHandle(hMenu);
}

/**
 * Adds a menu button to a menu. (See below for easier method)
 * 
 * @param hMenu         Menu to add button to.
 * @param menubtndata   Array containing button data.  See enum MenuLib_Button.
 * 
 * @return              Index of new button.
 */
stock MenuLib_AddMenuBtn(Handle:hMenu, menubtndata[MenuLib_Button])
{
    PushArrayArray(hMenu, menubtndata[0], sizeof(menubtndata));
    return GetArraySize(hMenu) - 2;
}

/**
 * Adds a menu button with different parameters.
 * 
 * @param hMenu             Menu to add button to.
 * @param label             What to display for this menu button.
 * @param info              Identical to the "info" parameter in AddMenuItem, see that.
 * @param translate         Instead of printing MenuBtn_Label, it will print the translation phrase it points to.
 * @param style             See SM include menus.inc ITEMDRAW_* defines.
 * @param callback          The function to call when this button is selected.  "" to call no function. <-  TODO
 * @param NextMenu          Where this menu button wants to link to.
 * @param strLinkedMenu     The id of the menu to link this button to if NextMenu = BtnNextMenu_LinkMenu, otherwise set to "".
 * 
 * @return                  Index of new button.
 */
stock MenuLib_AddMenuBtnEx(Handle:hMenu, String:label[], const String:info[], bool:translate, style, MenuLibBtnCallback:callback, MenuLib_BtnNextMenu:NextMenu, String:strLinkedMenu[])
{
    // Create array.
    new menubtndata[MenuLib_Button];
    strcopy(menubtndata[MenuBtn_Label], sizeof(menubtndata[MenuBtn_Label]), label);
    strcopy(menubtndata[MenuBtn_Info], sizeof(menubtndata[MenuBtn_Info]), info);
    menubtndata[MenuBtn_Translate] = translate;
    menubtndata[MenuBtn_Style] = style;
    menubtndata[MenuBtn_Callback] = callback;
    menubtndata[MenuBtn_NextMenu] = NextMenu;
    strcopy(menubtndata[MenuBtn_LinkedMenu], sizeof(menubtndata[MenuBtn_LinkedMenu]), strLinkedMenu);
    
    PushArrayArray(hMenu, menubtndata[0], sizeof(menubtndata));
    return GetArraySize(hMenu) - 2;
}

/**
 * Removes a menu button from the menu.
 * 
 * @param hMenu         Menu to add button to.
 * @param button        Index of the menu button.  (Starting from 0)
 */
stock MenuLib_RemoveButton(Handle:hMenu, button)
{
    if (button < 0 || button >= MenuLib_GetMenuBtnCount(hMenu))
        ThrowError("Invalid menu button index: %d", button);
    
    RemoveFromArray(hMenu, button+1);
}

/**
 * Removes all menu buttons from the menu.
 * 
 * @param hMenu         Menu to add button to.
 */
stock MenuLib_RemoveAllButtons(Handle:hMenu)
{
    while(GetArraySize(hMenu) > 1)
        RemoveFromArray(hMenu, 1);
}

/**
 * Returns the number of buttons a menu contains.
 * 
 * @param hMenu The menulib handle.
 * 
 * @error       Invalid menulib handle.
 */
stock MenuLib_GetMenuBtnCount(Handle:hMenu)
{
    return GetArraySize(hMenu) - 1;
}

/**
 * Menu data read/write functions.
 */

/**
 * Menu data reader that returns all available menu data.
 * 
 * @param hMenu         The menu whose data to read.
 * @param menudata      Output array for all menu data.  See enum MenuLib_Menu.
 */
stock MenuLib_MenuReadAll(Handle:hMenu, menudata[MenuLib_Menu])
{
    GetArrayArray(hMenu, 0, menudata[0], sizeof(menudata));
}

/**
 * Menu data reader for any data type except strings.
 * 
 * @param hMenu     The menu whose cell data to read.
 * @param data      The data to get the value of.  See enum MenuLib_Menu.
 * 
 * @return          The value of the desired menu data.
 */
stock MenuLib_MenuReadCell(Handle:hMenu, MenuLib_Menu:data)
{
    new menudata[MenuLib_Menu];
    MenuLib_MenuReadAll(hMenu, menudata);
    
    // Return the value.
    return _:menudata[data];
}

/**
 * Menu data reader for any string typed values.
 * 
 * @param hMenu     The menu whose string data to read.
 * @param data      The data to get the value of.  See enum MenuLib_Menu.
 * @param output    Output variable for the data read.
 * @param maxlen    The max length of the output string.
 */
stock MenuLib_MenuReadString(Handle:hMenu, MenuLib_Menu:data, String:output[], maxlen)
{
    new menudata[MenuLib_Menu];
    MenuLib_MenuReadAll(hMenu, menudata);
    
    // Copy full name to output
    strcopy(output, maxlen, String:menudata[data]);
}

/**
 * Menu data reader wrapper for reading a menulib menu's SM menu handle.
 * 
 * @param hMenu     The menu whose string data to read.
 */
stock Handle:MenuLib_MenuGetSMMenuHandle(Handle:hMenu)
{
    new menudata[MenuLib_Menu];
    MenuLib_MenuReadAll(hMenu, menudata);
    return menudata[MenuData_Handle];
}

/**
 * Menu data writer that overwrites all data for a menu with the given data.
 * 
 * @param hMenu         Menulib handle to write data to.
 * @param menudata      New data to replace the old data.  See enum MenuLib_Menu.
 */
stock MenuLib_MenuWriteAll(Handle:hMenu, menudata[MenuLib_Menu])
{
    SetArrayArray(hMenu, 0, menudata[0], sizeof(menudata));
}

/**
 * Menu data writer that writes a specified non-string data value.
 * 
 * @param hMenu     The menu whose cell data to write.
 * @param data      Data to write new value to.  See enum MenuLib_Menu.
 * @param value     Any cell value to write as the new data.
 */
stock MenuLib_MenuWriteCell(Handle:hMenu, MenuLib_Menu:data, any:value)
{
    // Read all the menu data.
    new menudata[MenuLib_Menu];
    MenuLib_MenuReadAll(hMenu, menudata);
    
    // Change the value of the specified menu data.
    menudata[data] = value;
    
    // Overwrite the old array with the modified one.
    MenuLib_MenuWriteAll(hMenu, menudata);
}

/**
 * Menu data writer that writes a specified string data value.
 * 
 * @param hMenu     The menu whose string data to write.
 * @param data      Data to write new string to.  See enum MenuLib_Menu.
 * @param maxlen    The max length of the data value.  See enum MenuLib_Menu.
 * @param value     A string to write as the new data value.
 */
stock MenuLib_MenuWriteString(Handle:hMenu, MenuLib_Menu:data, maxlen, const String:value[])
{
    // Read all the menu data.
    new menudata[MenuLib_Menu];
    MenuLib_MenuReadAll(hMenu, menudata);
    
    // Change the value of the specified menu data.
    strcopy(String:menudata[data], maxlen, value);
    
    // Overwrite the old array with the modified one.
    MenuLib_MenuWriteAll(hMenu, menudata);
}

/**
 * Menu data button read/write functions.
 */

/**
 * Menu data reader that returns all available menu button data.
 * 
 * @param hMenu         The menu whose data to read.
 * @param button        Index of the menu button.  (Starting from 0)
 * @param menubtndata   Output array for all menu data.  See enum MenuLib_Button.
 * 
 * @error               Invalid menu button index.
 */
stock MenuLib_BtnReadAll(Handle:hMenu, button, menubtndata[MenuLib_Button])
{
    if (button < 0 || button >= MenuLib_GetMenuBtnCount(hMenu))
        ThrowError("Invalid menu button index: %d", button);
    
    GetArrayArray(hMenu, button+1, menubtndata[0], sizeof(menubtndata));
}

/**
 * Menu button data reader for any data type except strings.
 * 
 * @param hMenu     The menu whose cell data to read.
 * @param button    Index of the menu button.  (Starting from 0)
 * @param data      The data to get the value of.  See enum MenuLib_Button.
 * 
 * @return          The value of the desired menu data.
 * @error           Invalid menu button index.
 */
stock MenuLib_BtnReadCell(Handle:hMenu, button, MenuLib_Button:data)
{
    if (button < 0 || button >= MenuLib_GetMenuBtnCount(hMenu))
        ThrowError("Invalid menu button index: %d", button);
    
    new menubtndata[MenuLib_Button];
    MenuLib_BtnReadAll(hMenu, button, menubtndata);
    
    // Return the value.
    return _:menubtndata[data];
}

/**
 * Menu button data reader for any string typed values.
 * 
 * @param hMenu     The menu whose string data to read.
 * @param button    Index of the menu button.  (Starting from 0)
 * @param data      The data to get the value of.  See enum MenuLib_Button.
 * @param output    Output variable for the data read.
 * @param maxlen    The max length of the output string.
 * 
 * @error           Invalid menu button index.
 */
stock MenuLib_BtnReadString(Handle:hMenu, button, MenuLib_Button:data, String:output[], maxlen)
{
    if (button < 0 || button >= MenuLib_GetMenuBtnCount(hMenu))
        ThrowError("Invalid menu button index: %d", button);
    
    new menubtndata[MenuLib_Button];
    MenuLib_BtnReadAll(hMenu, button, menubtndata);
    
    // Copy full name to output
    strcopy(output, maxlen, String:menubtndata[data]);
}

/**
 * Menu data writer that overwrites all data for a menu with the given data.
 * 
 * @param hMenu         Menulib handle to write data to.
 * @param button        Index of the menu button.  (Starting from 0)
 * @param menubtndata   New data to replace the old data.  See enum MenuLib_Button.
 * 
 * @error               Invalid menu button index.
 */
stock MenuLib_BtnWriteAll(Handle:hMenu, button, menubtndata[MenuLib_Button])
{
    if (button < 0 || button >= MenuLib_GetMenuBtnCount(hMenu))
        ThrowError("Invalid menu button index: %d", button);
    
    SetArrayArray(hMenu, button+1, menubtndata[0], sizeof(menubtndata));
}

/**
 * Menu data writer that writes a specified non-string data value.
 * 
 * @param hMenu     The menu whose cell data to write.
 * @param button    Index of the menu button.  (Starting from 0)
 * @param data      Data to write new value to.  See enum MenuLib_Button.
 * @param value     Any cell value to write as the new data.
 * 
 * @error           Invalid menu button index.
 */
stock MenuLib_BtnWriteCell(Handle:hMenu, button, MenuLib_Button:data, any:value)
{
    if (button < 0 || button >= MenuLib_GetMenuBtnCount(hMenu))
        ThrowError("Invalid menu button index: %d", button);
    
    // Read all the menu data.
    new menubtndata[MenuLib_Button];
    MenuLib_BtnReadAll(hMenu, button, menubtndata);
    
    // Change the value of the specified menu data.
    menubtndata[data] = value;
    
    // Overwrite the old array with the modified one.
    MenuLib_BtnWriteAll(hMenu, button, menubtndata);
}

/**
 * Menu button data writer that writes a specified string data value.
 * 
 * @param hMenu     The menu whose string data to write.
 * @param button    Index of the menu button.  (Starting from 0)
 * @param data      Data to write new string to.  See enum MenuLib_Button.
 * @param maxlen    The max length of the data value.  See enum MenuLib_Button.
 * @param value     A string to write as the new data value.
 * 
 * @error           Invalid menu button index.
 */
stock MenuLib_BtnWriteString(Handle:hMenu, button, MenuLib_Button:data, maxlen, const String:value[])
{
    if (button < 0 || button >= MenuLib_GetMenuBtnCount(hMenu))
        ThrowError("Invalid menu button index: %d", button);
    
    // Read all the menu data.
    new menubtndata[MenuLib_Button];
    MenuLib_BtnReadAll(hMenu, button, menubtndata);
    
    // Change the value of the specified menu data.
    strcopy(String:menubtndata[data], maxlen, value);
    
    // Overwrite the old array with the modified one.
    MenuLib_BtnWriteAll(hMenu, button, menubtndata);
}

/**
 * Displays a menulib-built menu to a client, resetting the previous menu stack, which guarantees this menu will have no "Back" button.
 * 
 * @param hMenu     The handle of the menu to send.
 * @param client    The client to send the menu to.
 */
stock MenuLib_DisplayMenu(Handle:hMenu, client)
{
    if (g_hPrevMenus[client] != INVALID_HANDLE)
        CloseHandle(g_hPrevMenus[client]);
    g_hPrevMenus[client] = CreateStack();
    
    MenuLib_SendMenu(Handle:hMenu, client);
}

/**
 * Send a menu to a client, without resetting the previous menu stack.
 *
 * The "Back" button on this menu will point back to the menu currently open on
 * their screen if available.
 * 
 * @param hMenu             The handle of the menu to send.
 * @param client            The client to send the menu to.
 * @param hFrom             The menu that linked client to this menu.  (For the back stack)
 */
stock MenuLib_SendMenu(Handle:hMenu, client, Handle:hFrom = INVALID_HANDLE)
{
    MenuLib_SendMenuAtItem(hMenu, client, 0, hFrom);
}

/**
 * Send a menu to a client at the specified item position, without resetting the
 * previous menu stack.
 *
 * The "Back" button on this menu will point back to the menu currently open on
 * their screen if available.
 *
 * @param hMenu             The handle of the menu to send.
 * @param client            The client to send the menu to.
 * @param firstItem         First item to begin drawing from.
 * @param hFrom             The menu that linked client to this menu.  (For the back stack)
 */
stock MenuLib_SendMenuAtItem(Handle:hMenu, client, firstItem, Handle:hFrom = INVALID_HANDLE)
{
    // Ensure that there is a prev menu stack available.
    if (g_hPrevMenus[client] == INVALID_HANDLE)
        g_hPrevMenus[client] = CreateStack();
    
    // Check if this menu was linked by another menu.
    if (hFrom != INVALID_HANDLE)
        PushStackCell(g_hPrevMenus[client], hFrom);
    
    // Call the pre callback for this menu.
    new MenuLibPreCallback:precallback = MenuLibPreCallback:MenuLib_MenuReadCell(hMenu, MenuData_PreCallback);
    if (precallback != INVALID_FUNCTION)
    {
        Call_StartFunction(GetMyHandle(), precallback);
        Call_PushCell(hMenu);
        Call_PushCell(client);
        Call_Finish();
    }
    
    // Get menu data.
    new menudata[MenuLib_Menu];
    MenuLib_MenuReadAll(hMenu, menudata);
    
    decl String:title[ML_DATA_TITLE];
    if (menudata[MenuData_Translate])   Format(title, sizeof(title), "%T", menudata[MenuData_Title], client);
    else                                strcopy(title, sizeof(title), menudata[MenuData_Title]);
    
    // Create menu and set title.
    new Handle:hSMMenu = CreateMenu(MenuLib_Handler);
    SetMenuTitle(hSMMenu, title);
    
    // Add exit back button if applicable.
    SetMenuExitBackButton(hSMMenu, !IsStackEmpty(g_hPrevMenus[client]) || menudata[MenuData_ForceBack]);
    
    decl String:strHandle[16];
    IntToString(_:hMenu, strHandle, sizeof(strHandle));
    
    // Add menu items.
    decl String:buttontext[ML_DATA_LABEL];
    new count = GetArraySize(hMenu);
    if (count <= 1) // We need some way to identify which menulib handle is associated with this SM menu.
        AddMenuItem(hSMMenu, strHandle, " ", ITEMDRAW_DISABLED);
    else
    {
        new menubtndata[MenuLib_Button];
        for (new bindex = 1; bindex < count; bindex++)
        {
            GetArrayArray(hMenu, bindex, menubtndata[0], sizeof(menubtndata));
            
            
            if (menubtndata[MenuBtn_Translate])
                Format(buttontext, sizeof(buttontext), "%T", menubtndata[MenuBtn_Label], client);
            else
                strcopy(buttontext, sizeof(buttontext), menubtndata[MenuBtn_Label]);
            AddMenuItem(hSMMenu, strHandle, buttontext, menubtndata[MenuBtn_Style]);
        }
    }
    
    // Put SM menu handle into the menulib menu data.
    menudata[MenuData_Handle] = hSMMenu;
    MenuLib_MenuWriteAll(hMenu, menudata);
    
    DisplayMenuAtItem(hSMMenu, client, firstItem, MENU_TIME_FOREVER);
}

/**
 * Re-sends the previous menu the client had.  Note that this will work even if there is no menu on their screen.
 * If there is no previous menu this function will do nothing.
 * 
 * @param client    Client to perform action on.
 */
stock MenuLib_MenuGoBack(client)
{
    // Open previous menu.
    new Handle:hPrevMenu;
    if (PopStackCell(g_hPrevMenus[client], hPrevMenu))
        MenuLib_SendMenu(hPrevMenu, client);
}

public MenuLib_Handler(Handle:menu, MenuAction:action, client, slot)
{
    // Resolve string into the handle of the menu this menu button is from.
    decl String:strHandle[16];
    GetMenuItem(menu, 0, strHandle, sizeof(strHandle));
    new Handle:hMenu = Handle:StringToInt(strHandle);   // Handle of menulib menu, not SM menu handle.
    
    // Get menu data.
    new menudata[MenuLib_Menu];
    MenuLib_MenuReadAll(hMenu, menudata);
    
    if (action == MenuAction_Select)
    {
        // Get menu button data.
        new menubtndata[MenuLib_Button];
        MenuLib_BtnReadAll(hMenu, slot, menubtndata);
        
        switch(menubtndata[MenuBtn_NextMenu])
        {
            case BtnNextMenu_None:
            {
            }
            case BtnNextMenu_LinkMenu:
            {
                new Handle:hLinkedMenu = MenuLib_FindMenuById(menubtndata[MenuBtn_LinkedMenu]);
                if (hLinkedMenu != INVALID_HANDLE)
                    MenuLib_SendMenu(hLinkedMenu, client, menudata[MenuData_Temp] ? INVALID_HANDLE: hMenu); // Temp menus can't be linked back to.
            }
            case BtnNextMenu_LinkCurrent:
            {
                if (menudata[MenuData_Temp])
                    ThrowError("Temporary menus can't link to themselves!");
                
                // Resend current menu.
                MenuLib_SendMenu(hMenu, client);
            }
            case BtnNextMenu_LinkBack:
            {
                // If there are previous menus to go to, then jump back, otherwise resend this menu.
                if (g_hPrevMenus[client] != INVALID_HANDLE && !IsStackEmpty(g_hPrevMenus[client]))
                    MenuLib_MenuGoBack(client);
                else
                    MenuLib_SendMenu(hMenu, client);
            }
        }
        
        // Call the callback for this menu.
        if (menudata[MenuData_Callback] != INVALID_FUNCTION)
        {
            Call_StartFunction(GetMyHandle(), menudata[MenuData_Callback]);
            Call_PushCell(hMenu);
            Call_PushCell(action);
            Call_PushCell(client);
            Call_PushCell(slot);
            Call_Finish();
        }
        
        // Call the callback for this button.
        if (menubtndata[MenuBtn_Callback] != INVALID_FUNCTION)
        {
            Call_StartFunction(GetMyHandle(), menubtndata[MenuBtn_Callback]);
            Call_PushCell(hMenu);
            Call_PushCell(client);
            Call_PushCell(slot);
            Call_Finish();
        }
    }
    else if (action == MenuAction_Cancel)
    {
        if (slot == MenuCancel_ExitBack)
        {
            if (!menudata[MenuData_ForceBack])
                MenuLib_MenuGoBack(client);
        }
        
        // Call the callback for this menu.
        if (menudata[MenuData_Callback] != INVALID_FUNCTION)
        {
            Call_StartFunction(GetMyHandle(), menudata[MenuData_Callback]);
            Call_PushCell(hMenu);
            Call_PushCell(MenuAction_Cancel);
            Call_PushCell(client);
            Call_PushCell(slot);
            Call_Finish();
        }
    }
    else if (action == MenuAction_End)
    {
        if (menudata[MenuData_Callback] != INVALID_FUNCTION)
        {
            Call_StartFunction(GetMyHandle(), menudata[MenuData_Callback]);
            Call_PushCell(hMenu);
            Call_PushCell(MenuAction_End);
            Call_PushCell(client);
            Call_PushCell(slot);
            Call_Finish();
        }
        
        // Update menu data in case something was changed in the callback.
        MenuLib_MenuReadAll(hMenu, menudata);
        
        if (menudata[MenuData_Temp])
            MenuLib_EradicateMenu(hMenu);
        else
        {
            menudata[MenuData_Handle] = INVALID_HANDLE;
            MenuLib_MenuWriteAll(hMenu, menudata);
        }
        CloseHandle(menu);
    }
    else
    {
        if (menudata[MenuData_Callback] != INVALID_FUNCTION)
        {
            Call_StartFunction(GetMyHandle(), menudata[MenuData_Callback]);
            Call_PushCell(hMenu);
            Call_PushCell(action);
            Call_PushCell(client);
            Call_PushCell(slot);
            Call_Finish();
        }
    }
}

/**
 * Work around until I can check the top value with SM.
 */
stock Handle:StackTopCell(Handle:stack)
{
    if (IsStackEmpty(stack))
        return INVALID_HANDLE;
    
    new Handle:value;
    PopStackCell(stack, value);
    PushStackCell(stack, value);
    return value;
}

/**
 * Creates a temporary menulib menu to show a list of clients.
 * 
 * @param client            Who to send menu to.
 * @param title             The title of the temporary menu.
 * @param translate         True to translate title, false if literal string.
 * @param forceback         Force the back button to appear in the menu.
 * @param precallback       Called right before the menu is displayed to a client.  INVALID_FUNCTION for no callback.
 * @param callback          Menu callback.  See MenuLib_CreateMenu.
 * @param NextMenu          Where all buttons in this menu will link to.
 * @param strLinkedMenu     The id of the menu to link this button to if NextMenu = BtnNextMenu_LinkMenu, otherwise set to "". 
 * @param strFilterFunc     The handle of the menu to link this button to if NextMenu = BtnNextMenu_LinkMenu, otherwise INVALID_HANDLE.
 * @param filters           A bit field made up of UTILS_FILTER_* defines.  See utilities.inc.
 *                          Note: This is only checked at time of menu creation.  It is recommended to re-check these filters in the callback.
 * @param filter            Name of a function with the structure shown in the above comments that allows changing and pruning menu buttons.  Leave blank for no filter.
 * 
 * @return                  A handle to the temporary menu.  This will become invalid as soon as the client receives and closes the menu.
 * @error                   Invalid function given for either the callback or filter function.
 */
stock Handle:MenuLib_CreateClientListMenu(client, const String:title[], bool:translate, bool:forceback, MenuLibPreCallback:precallback, MenuLibCallback:callback, MenuLib_BtnNextMenu:NextMenu, String:strLinkedMenu[], filters, MenuLibClientFilter:filter = INVALID_FUNCTION)
{
    // Create a temporary menu with a unique ID.
    decl String:id[64];
    new counter;
    do // Find a unique ID.  If this menu is resent before the temp one is killed we need to find an ID that is unique.  This loop will allow infinite nesting of these menus.
    {
        Format(id, sizeof(id), "clientlistmenu_%N_%d", client, counter++);
    } while (MenuLib_FindMenuById(id) != INVALID_HANDLE);
    
    new Handle:hTempMenu = MenuLib_CreateMenu(id, precallback, callback, title, translate, forceback, true);
    
    decl String:clientoption[256];
    decl String:clientuserid[256];
    new menuclient;
    
    new Handle:adtClients;
    new count = Util_BuildClientList(adtClients, filters, client);
    for (new cindex = 0; cindex < count; cindex++)
    {
        // Get the client index from the array.
        menuclient = GetArrayCell(adtClients, cindex);
        
        // Get client info.
        GetClientName(menuclient, clientoption, sizeof(clientoption));
        IntToString(GetClientUserId(menuclient), clientuserid, sizeof(clientuserid));
        
        // Call the filter function and return proper values.
        if (filter != INVALID_FUNCTION)
        {
            if (!MenuLib_ClientMenuCallFilter(hTempMenu, filter, menuclient, clientoption, sizeof(clientoption), clientuserid, sizeof(clientuserid)))
                continue;
        }
        
        // Add option to menu.
        MenuLib_AddMenuBtnEx(hTempMenu, clientoption, clientuserid, false, ITEMDRAW_DEFAULT, INVALID_FUNCTION, NextMenu, strLinkedMenu);
    }
    CloseHandle(adtClients);
    return hTempMenu;
}

/**
 * Retrieves the client index given a menu slot from a MenuLib_ClientMenu-constructed menu.
 * Must be used from inside the menu's callback.
 * 
 * @param hMenu         The handle to the menulib menu.
 * @param button        Index of the menu button.  (Starting from 0)
 * 
 * @return              The client index, 0 if the selected client is no longer in the server.
 */
stock MenuLib_GetClientIndex(Handle:hMenu, button)
{
    // Get string stored in the menu slot.
    decl String:clientuserid[8];
    MenuLib_BtnReadString(hMenu, button, MenuBtn_Info, clientuserid, sizeof(clientuserid));
    
    // Return the targeted client through their userid stored as a string in the menu slot.
    return GetClientOfUserId(StringToInt(clientuserid));
}

/**
 * Calls a filter function and determines appropriate values to return based on returned hook action.
 * 
 * @param hMenu         The menu the clients are being added to.
 * @param filter        Filter function name.
 * @param client        Any cell value to pass to the filter function.
 * @param buttontxt     The filter function will possibly change this value but will only be passed back if Plugin_Changed is returned.
 * @param buttoninfo    The filter function will possibly change this value but will only be passed back if Plugin_Changed is returned.
 * 
 * @return              True if the action should be completed, false if the filter asked to block action.
 */
stock bool:MenuLib_ClientMenuCallFilter(Handle:hMenu, MenuLibClientFilter:filter, client, String:buttontxt[], maxlen, String:buttoninfo[], maxlen2)
{
    decl String:newbuttontxt[256];
    decl String:newbuttoninfo[256];
    strcopy(newbuttontxt, sizeof(newbuttontxt), buttontxt);
    strcopy(newbuttoninfo, sizeof(newbuttoninfo), buttoninfo);
    
    Call_StartFunction(GetMyHandle(), filter);
    Call_PushCell(hMenu);
    Call_PushCell(client);
    Call_PushStringEx(newbuttontxt, sizeof(newbuttontxt), SM_PARAM_STRING_UTF8|SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK);
    Call_PushStringEx(newbuttoninfo, sizeof(newbuttoninfo), SM_PARAM_STRING_UTF8|SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK);
    new Action:result;
    Call_Finish(result);
    
    // Interpret hook return.
    if (result == Plugin_Continue) return true;
    else if (result == Plugin_Changed)
    {
        strcopy(buttontxt, maxlen, newbuttontxt);
        strcopy(buttoninfo, maxlen2, newbuttoninfo);
        return true;
    }
    return true;
}

// ***********************************
//      Miscellaneous Utilities
// ***********************************

/**
 * Converts a boolean value into a menu draw define.
 * true =   ITEMDRAW_DEFAULT
 * false =  ITEMDRAW_DISABLED
 * 
 * @param condition The boolean to convert. 
 */
stock MenuLib_GetMenuItemDraw(bool:condition)
{
    return condition ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED;
}
